package com.gframework.mybatis.config;

import java.io.FileNotFoundException;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import javax.sql.DataSource;

import org.apache.ibatis.binding.MapperRegistry;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.InterceptorChain;
import org.apache.ibatis.reflection.MetaClass;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.ReflectionException;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.mybatis.spring.boot.autoconfigure.MybatisProperties;
import org.mybatis.spring.mapper.MapperScannerConfigurer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.lang.Nullable;

import com.gframework.mybatis.dao.IMybatisDAO;
import com.gframework.mybatis.dao.MybatisDaoAop;
import com.gframework.mybatis.dao.mybatis.provider.core.ExtendMapperRegistry;
import com.github.pagehelper.util.MetaObjectUtil;

/**
 * mybatis配置.
 * <p>本类会读取默认的mybatis的配置信息，并以默认数据源去配置，你可以通过使用{@link MapperScan}注解来对自己的dao进行扫描
 * <p>你可以通过本类来获取mybatis的一些全局配置类，包括{@link Configuration}和{@link MetaObject}
 * <p>本类力争做到全局只有唯一的{@link Configuration} 和
 * {@link MetaObject}。PageHelper组件自身的MetaObject也已经被替换成mybatis默认的了。不会出现重复。
 * 
 * @since 1.0.0
 * @author Ghwolf
 * 
 * @see MetaObjectUtil
 */
@org.springframework.context.annotation.Configuration(proxyBeanMethods = false)
@ConditionalOnClass(DataSource.class)
@ConditionalOnBean(DataSourceAutoConfiguration.class)
@Import(MybatisDaoAop.class)
public class MybatisConfig {
	private static final Logger logger = LoggerFactory.getLogger(MybatisConfig.class);

	/** 基础框架xml所在目录，可以配置多个，然后解析成多个。ANT表达式，支持jar包 */
	private static final String GFRAMEWORK_MAPPER_LOCATIONS = "classpath:com/gframework/biz/**/dao/*.xml";
	/** 基础框架dao所在包，多个用{@link ConfigurableApplicationContext#CONFIG_LOCATION_DELIMITERS} 字符分割 */
	private static final String GFRAMEWORK_BASE_PACKAGES = "com.gframework.biz.**.dao";

	
	/**
	 * 缓存mybatis的配置信息，让任何地方都可以获取到
	 */
	private static Configuration configuration;
	
	/**
	 * 取得mybatis的配置信息
	 */
	public static Configuration getConfiguration() {
		return configuration;
	}

	/**
	 * 此方法使用来优化PageHelper的 MetaObjectUtil类操作的，让其也可以获取到mybatis自己缓存起来的对象信息，以节约空间。
	 */
	public static MetaObject newMetaObject(Object obj) {
		return configuration.newMetaObject(obj);
	}

	/**
	 * 根据class类型取得一个MetaClass类对象，相对于MetaObject,MetaClass更加轻量，但是没有提供过于复杂的功能，
	 * 如果只是针对bean自己的属性的一些setter或getter操作，那么使用MetaClass就可以了。
	 * <p>
	 * MetaObject本身也使用了MetaClass，但是他对name的处理提供了一个表达式解析的操作。
	 * <p>
	 * 这个MetaClass是有缓存的，它基于Configuration的ReflectorFactory
	 * 
	 * @param type 要解析的类型
	 * @return 返回MetaClass类对象。
	 */
	public static MetaClass getMetaClass(Class<?> type) {
		return MetaClass.forClass(type, configuration.getReflectorFactory());
	}

	/**
	 * 优化MetaObject，和PageHelper的MetaObjectUtil类，
	 * 让PageHelper也使用Configuration的newMetaObject方法。
	 */
	private static void optimizeMetaObject() {
		try {
			// MetaObjectWithReflectCache类以无用，但是存在三个全局常量未回收
			MetaObjectUtil.method = MybatisConfig.class.getDeclaredMethod("newMetaObject", Object.class);
		} catch (Exception e) {
			throw new ReflectionException(e);
		}
	}
	/**
	 * 防止mybatis插件重复
	 */
	private static void interceptorRepeatIssueHandler(Configuration configuration){
		try {
			Field interceptorChainField = configuration.getClass().getDeclaredField("interceptorChain");
			interceptorChainField.setAccessible(true);
			InterceptorChain chain = (InterceptorChain) interceptorChainField.get(configuration);
			interceptorChainField.set(configuration, new NoRepeatInterceptorChain(chain));
		} catch (Exception e) {
			logger.warn("mybatis插件重复兼容性问题处理异常，或许是因为版本问题导致，但并不影响程序正常运行：{}",e.getMessage());
		}
	}
	
	static class NoRepeatInterceptorChain extends InterceptorChain {
		
		public NoRepeatInterceptorChain(InterceptorChain chain) {
			for (Interceptor inter : chain.getInterceptors()) {
				super.addInterceptor(inter);
			}
		}
		
		@Override
		public void addInterceptor(Interceptor interceptor) {
			if (!super.getInterceptors().contains(interceptor)) {
				super.addInterceptor(interceptor);
			}
		}
	}
	
	
	/**
	 * 构造一个SqlSessionFactory，并且共用同一个Confiuration配置类和mybatis插件
	 * @param datasource 数据源
	 * @param mapperLocations xml扫描路径
	 */
	public static synchronized SqlSessionFactory createSqlSessionFactory(MybatisProperties properties,
			DataSource datasource, String... mapperLocations) throws Exception {
		if (configuration == null) {
			configuration = properties.getConfiguration();
			
		    if (configuration == null) {
		      configuration = new Configuration();
		    }
			
			interceptorRepeatIssueHandler(configuration);
			optimizeMetaObject();
			

			// 替换MapperRegistry，扩展一个由于dao继承多子接口复用（既多表共用dao方法）导致的无法唯一确定主键的问题
			MapperRegistry mapperRegistry = new ExtendMapperRegistry(configuration);
			Field f = Configuration.class.getDeclaredField("mapperRegistry");
			f.setAccessible(true);
			f.set(configuration, mapperRegistry);
		}
		
		SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
		bean.setConfiguration(configuration);
		bean.setDataSource(datasource);
		// 利用路径转换器将通配符路径转换为Resource数组
		ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();

		List<Resource> resourceList = new ArrayList<>();
		if (mapperLocations != null && mapperLocations.length > 0) {
			for (String mapperLocation : mapperLocations) {
				try {
					Collections.addAll(resourceList, resolver.getResources(mapperLocation));
				} catch(FileNotFoundException e) {
					logger.warn(e.getMessage());
				}
			}
		}
		bean.setMapperLocations(resourceList.toArray(new Resource[resourceList.size()]));

		return bean.getObject();
	}

	/**
	 * 构造一个 MapperScannerConfigurer 类对象
	 * @param markerInterface mybatis dao扫描接口，可以为null
	 * @param basePackage 扫描包，例如：com.gframework.**.dao
	 * @param sqlSessionFactoryBeanName sqlSessionFactory bean名称
	 * @return 返回 MapperScannerConfigurer 类对象
	 */
	public static MapperScannerConfigurer getMapperScannerConfigurer(@Nullable Class<?> markerInterface, String basePackage,
			String sqlSessionFactoryBeanName) {
		MapperScannerConfigurer conf = new MapperScannerConfigurer();
		if (markerInterface != null) {
			conf.setMarkerInterface(markerInterface);
		}
		conf.setBasePackage(basePackage);
		conf.setSqlSessionFactoryBeanName(sqlSessionFactoryBeanName);
		return conf;
	}
	
	/**
	 * 创建SqlSessionFactoryBean.<br>
	 * 需要指定mybatis配置文件位置以及xml映射文件所在路径。
	 * 
	 * @param properties 这个是spring自动装配的bean，需要通过他获取Configuration类对象
	 */
	@Bean("gframework-default-sqlSessionFactory")
	@Primary
	public SqlSessionFactory getSqlSessionFactoryBean(MybatisProperties properties, DataSource datasource) throws Exception {
		String[] ml = properties.getMapperLocations();
		List<String> mlList = new ArrayList<>();
		mlList.add(GFRAMEWORK_MAPPER_LOCATIONS);
		if (ml != null && ml.length != 0) {
			Collections.addAll(mlList, ml);
		}
		return createSqlSessionFactory(properties,datasource,mlList.toArray(new String[mlList.size()]));
	}

	@Bean("gframework-default-mapper-scanner")
	public static MapperScannerConfigurer getMapperScannerConfigurer() {
		StringBuilder bp = new StringBuilder(GFRAMEWORK_BASE_PACKAGES) ;
		return getMapperScannerConfigurer(IMybatisDAO.class, bp.toString(), "gframework-default-sqlSessionFactory");
	}
	

}
